require 'rexml/document'

module PNEngine
  class Transition < ActiveRecord::Base
    include BindingsHelper
    include Resource::InstanceMethods
    extend Resource::ClassMethods
    
    belongs_to :petrinet
    has_many :arcs
    has_many :combinations, :dependent => :delete_all
    has_many :modtokens, :dependent => :delete_all
    has_many :transition_modes, :dependent => :delete_all

    #returns true if combination's active, passes options hash 
    def active?(options = {})
      if afterdeploy
        token_combinations(true)
        self.afterdeploy = false
        save!
      end
      
      return (!self.has_incoming_places? || active_combinations(options).size > 0)
    end
    
    #returns all valid combinations of tokens
    #options:  :case => returns only token combinations of given case
    def active_combinations(options = {})
      combs = token_combinations.
        collect{|comb| comb.tokens}.
        select{|tokens| self.is_enabled?(tokens)}
      if options[:case]
        return combs.select do |tokens|
          tokens_with_case = tokens.select{|token| !token.case.nil?}
          !tokens_with_case.empty? && (tokens_with_case.first.case == options[:case])
        end
      else
        return combs
      end
    end
	
    def url(options = {})
      u = "/transitions/#{id}"
      if options[:case]
        u += "/case/#{options[:case].id}"
      elsif options[:tokens]
        u += '/tokens/' + options[:tokens].collect {|token| token.id}.join("/")
      end
      u = Merb::Config[:path_prefix] + u if Merb::Config[:path_prefix]
      u
    end

    def absolute_url(options = {})
      Merb::Config[:root] + url(options)
    end
    
    # Returns all places from incoming arcs.
    # Use arc_type to define only a subset of wanted places.
    # Use include_tokens, if tokens should be included.
    # Use for_combinations_only, if only places should be found which should be used in combinations.
    def incoming_places(options = {})
      where = ["arcs.from_is_place = true AND transition_id=? ", self.id]
      if options[:arc_type]
        @incoming_places_for_arc_type ||= {}
        return @incoming_places_for_arc_type[options[:arc_type]] if @incoming_places_for_arc_type[options[:arc_type]]
        where[0] += "AND arcs.type = ?"
        where << options[:arc_type].to_s.split("::").last
      elsif options[:for_combinations_only]
        return @incoming_places_for_combinations if !@incoming_places_for_combinations.blank?
        where[0] += "AND (arcs.type IS NULL OR arcs.type = 'ReadArc')"
      else
        return @incoming_places if !@incoming_places.blank?
      end
      
      #Merb.logger.error("incoming places")
      #Merb.logger.error(caller.first)
      
      include = [:arcs]
      if options[:include_tokens]
        include << :tokens
      end
      #result = arcs.find(:all, :conditions => where, :include => include).collect{|a|a.place}
      
      result = PNEngine::Place.find(:all, :include => include, :conditions => where)
      
      if options[:for_combinations_only]
        @incoming_places_for_combinations = result
      elsif options[:arc_type]
        @incoming_places_for_arc_type[options[:arc_type]] = result
      else
        @incoming_places = result
      end
      return result
    end
    
    def outgoing_places
      arcs.find(:all, :conditions => ["arcs.from_is_place = false"], :include => [:place]).collect{|a|a.place}
    end
    
    def consume(token)
      arc = Arc.find(:first, :conditions => ["place_id = ? AND transition_id = ?",token.place.id,self.id])
      if Merb::Config[:use_reevaluation]
        Merb.logger.info("Before deactivation: #{token.is_active}, #{token.place.tokens.size}")
        token.set_active(false) unless arc.kind_of?(ReadArc)
        Merb.logger.info("After deactivation: #{token.is_active}, #{token.place.tokens.size}")
      else
        token.place.pull(token) unless arc.kind_of?(ReadArc)
      end      
    end
    
    #returns true if the arc can be connected with the transition
    #always returns true
    #can be overriden in derived classes
    def validate_arc(arc)
      true
    end

    def xslt_path
      File.join(base_path, 'main_xslt.xsl')
    end 
    
    def base_path
      File.join(Merb::Config[:data_path], 'trans', id.to_s)
    end
    
    def xml_path
      File.join(base_path, 'main_xml.xml')
    end 
    
    # consumes tokens specified in pull_tokens (Array of tokens)
    # options:
    # * notify_following_transitions: boolean (default true), flag to use if 
    # following transitions are notified manually
    def fire(pull_tokens, attributes, options = {})
      options = {:notify_following_transitions => true}.merge(options) 

      Transition.transaction do
        begin
          #TODO: token size should be evaluated while considering inhibitor + reset arcs
          #raise "Size of pull_tokens doesn't equal size of incoming arcs" if pull_tokens.nil? || pull_tokens.size != incoming_places.size
          return if !is_enabled?(pull_tokens)

          yield if block_given?

          if !pull_tokens.empty?
            current_case = pull_tokens.first.case
          else
            current_case ||= PNEngine::Case.create!
            current_case.fillWithStandardTokens(self.petrinet.id)
          end
          
          time_begin = Time.now
          
          # Consume tokens from standard arcs.
          pull_tokens.each do |token|
            self.consume(token)            
          end
          
          # Reset places from reset arcs
          reset_incoming_places(current_case)

          # produce token for each outgoing arc
          produced_tokens = arcs.select {|arc| !arc.from_is_place}.collect do |arc|
            arc.push(PNEngine::Token.new(attributes, current_case))
          end.flatten
          if name == 'noname' && action.nil?
            action = 'allocate'
          end
          
          if Merb::Config[:use_reevaluation]
            #create transition mode          
            transition_modes << PNEngine::TransitionMode.new({:input_tokens => pull_tokens,
                  :output_tokens => produced_tokens, :execution_container => current_case.execution_log})
          end
          
          PNEngine::CsvLogger.log_transition("manual_fire", Time.new.to_s, "", id, name, action, current_case.id, petrinet.id)
          Merb.logger.info("[#{log_name}] Fired.")

          save!
          
          $EXECLOG[current_case.id] ||= []
          $EXECLOG[current_case.id] << [self, time_begin, Time.now]
          
          notify_following_transitions if options[:notify_following_transitions]
          return produced_tokens
        rescue Exception => ex
          Merb.logger.error("[#{self.log_name}] #{ex}")
          Merb.logger.error("[#{self.log_name}] #{ex.backtrace.join("\n")}")
          raise ex
        end
      end
    end
    
    #TODO more direct SQL, eager loading
    def following_transitions
      #SQL isn't faster, although working!!
      #Transition.find_by_sql(["SELECT DISTINCT t2.* FROM arcs AS a1, arcs AS a2, places, transitions AS t1, transitions AS t2 WHERE a1.from_is_place = false AND a2.from_is_place = true AND a1.transition_id=? AND a1.place_id = a2.place_id AND t2.id=a2.transition_id", self.id])
      outgoing_places.collect do |place|
        place.arcs.select {|arc| arc.place_to_transition? }.collect do |outgoing_arc|
          outgoing_arc.transition
        end
      end.flatten.compact
    end
    
    def notify_following_transitions
      Merb.logger.info("[#{log_name}] Notifies following transitions: #{following_transitions.select{|t|t.respond_to?(:trigger_fire)}.collect{|t|"#{t.log_name}"}.join(" | ")}. Transitions which aren't notified: #{following_transitions.select{|t|!t.respond_to?(:trigger_fire)}.collect{|t|"#{t.log_name}"}.join(" | ")}")
      following_transitions.each do |t| 
        t.trigger_fire if t.respond_to?(:trigger_fire)
      end
    end
    
    # Returns if Transition given with a combination of tokens is enabled.
    # This method calls eval_guard (perhaps only look up in combination table?)
    # and evaluate special kinds of arcs (e.g. inhibitors)
    def is_enabled?(tokens)
      return false if !eval_guard(tokens)
      # checks, whether any inhibited place have a token of current case.
      if self.has_inhibitor?
        #first tests with direct SQL query didn't show much performance boosts, should be evaluated
        count = PNEngine::Token.count_by_sql(["
          SELECT COUNT(tokens.*) 
          FROM tokens, places, arcs, transitions 
          WHERE transitions.id=? AND 
            arcs.from_is_place=true AND arcs.transition_id=? AND 
            arcs.place_id=places.id AND arcs.type='InhibitorArc' AND 
            tokens.place_id=places.id AND tokens.case_id=?", self.id, self.id, tokens.first.case.id])
        return false if count > 0
        #=======
        #inhibited_places = incoming_places(
        #  :arc_type => PNEngine::InhibitorArc
        #)
        #inhibited_places.each do |place|
        #  if place.tokens.count(:conditions => ["case_id = ?", tokens.first.case.id]) > 0
        #    return false
        #  end
        #end
      end
      return true
    end
    
    #Eval transition's guard. Tokens are these ones, which 
    #should be consumed from input places   
    def eval_guard(tokens)
      
      case_ids = []
      tokens.each do |token|
        if !token.case.nil?
          case_ids << token.case.id
        end
      end
      
      return false if case_ids.uniq.size > 1
            
      return true unless guard  
      
      g = guard.clone
      g.gsub!(/\s+/, " ")
      tokens.each do |token|
        token.values.each do |name, value|
          value = "\"#{value}\"" if value.is_a? String
          value = "0" if value.nil?
          #TODO who have added line below? causes a bug => uncommented
          #g.gsub!(/(^| )#{name}/, " #{value}")
          g.gsub!("#{token.place.pid}.#{name}", "#{value}")
        end
      end

      return eval(g)
                
    end
    
    #Evaluate transition's role.
    def eval_role
      
      return true unless role
      
      if $user          
        #iterate over the user's roles and look whether the user has the required role 
        $user.roles.each do |roles|
          #the user has the required role
          if (roles.name == role)
            #look whether he has permission 
            return roles.canFireTransition(self.action)
          end
        end
        #the user has not the required role
        return false
      end     
            
    end
     
    #evaluate whether the user is the right executer for the transition
    def eval_owner

      t_combinations = self.token_combinations
      
      if !(t_combinations.empty?)
        t_combinations.each do |comb|

          #if a token on the incoming place exist, the user of this token is evaluated
          if !(comb.tokens.empty?)
            comb.tokens.each do |token|
            
              if (token.place.contenttype == "flow")  
                
                value = getValueFromXML("owner", token.value)

                if (value && (value != ""))
                  if ($user.name == value)
                    return true
                  end
                else
                  if ((value == "") || (!value))
                    return eval_role
                  end
                end
              end
            end
            return false 
          end
        end
        return eval_role
      else        
        return eval_ownerFromContextPlace
      end   
    end
    
    #get the user from the contextplace and 
    #evaluate whether the user is the right executer for the transition
    def eval_ownerFromContextPlace
      
      #get the user from the context place
      vals = {}
      place = Place.find(:first, :conditions => ["name = ? and petrinet_id = ?", self.context_place_id, self.petrinet_id])
      token = Token.find(:first, :conditions => ["place_id = ?", place.id]) if place

      if token        
        if ($user.name == getValueFromXML("owner", token.value))
          return true
        else
          return false
        end
      else
        return true
      end

    end
 
    #return a value of a given tag from a given xml
    #!!!tag must exist as a locator on the contextPlace!!!
    def getValueFromXML(tag, xml)
      
      #get the user from the context place
      vals = {}
      place = Place.find(:first, :conditions => ["name = ? and petrinet_id = ?", self.context_place_id, self.petrinet_id])
      
      if place
        place.locators.collect do |locator|
          vals[locator.name] = locator.evaluate(xml)
        end
      end
      
      #iterate over all locators and find out the user's tag and it's value
      #look whether the user is the executer    
      if place
        vals.each do |name, value|
          # token.values.each do |name, value|
          if (name == tag )
            return value
          end
        end
        
        return nil
      end
      return nil
    end
    
    #get the Owner from a token
    def getOwner
      
      if !(self.token_combinations.empty?)
        
        self.token_combinations.each do |comb|
  
          if !(comb.tokens.empty?)
            comb.tokens.each do |token|
            
              if (token.place.contenttype == "flow")  
                return getValueFromXML("owner", token.value)
              end
            end
            
            return nil
          else
            return nil
          end
        end
      else
        return nil  
      end

    end
    
    #get only these tokens, which the user can execute
    def getUserValidToken
      
      validTokens = []
      
      t_combinations = self.token_combinations
      
      if !(t_combinations.empty?)
        t_combinations.each do |comb|
          
          if !(comb.tokens.empty?)
            comb.tokens.each do |token|
          
              if (token.place.contenttype == "flow")  
     
                value = getValueFromXML("owner", token.value)

                if value
                  if ($user.name == value)
                  validTokens << token
                  end
                end
                
              end
              
            end
          end
        end
        
        return validTokens
      end
      
      validTokens
    end    
  
    def to_pnml(as_document = false)
      #TODO: add xforms elements		
      
      pnml_doc = REXML::Document.new
      ptransition = pnml_doc.add_element('transition', {'id' => pid})
      
      #add name if specified
      if name
        pname = ptransition.add_element('name')
        ptext = pname.add_element('text')
        ptext.text = name
      end
      
      if !manual_execution || guard || role
        toolspecific = ptransition.add_element('toolspecific', {'tool' => 'Petri Net Engine', 'version' => '1.0'})
        
        #add automatic fire tag if automatic fire is enabled
        unless manual_execution
          attribute_hash = {'type' => 'automatic'}
          attribute_hash.merge!({'href' => xslt_url}) if xslt_url
          
          toolspecific.add_element('fire', attribute_hash)
        end
        
        if guard
          pguard = toolspecific.add_element('guard')
          pguard_expr = pguard.add_element('expr')
          pguard_expr.text = guard
        end
        
        if role
          prole = toolspecific.add_element('role')
          prole.text = role
        end
         
        if owner
          powner = toolspecific.add_element('owner')
          powner.text = owner
        end
      end
      
      output = ''
      as_document ? pnml_doc : pnml_doc.write(output, 2, true)
    end
    
    protected 
    before_create :label_transition
    def label_transition
      self.name = self.pid if self.name.blank?
    end
    
    def reset_incoming_places(current_case)
      PNEngine::Token.find_by_sql(["
        SELECT tokens.* 
        FROM tokens, places, arcs, transitions 
        WHERE transitions.id=? AND 
          arcs.from_is_place=true AND arcs.transition_id=? AND 
          arcs.place_id=places.id AND arcs.type='ResetArc' AND 
          tokens.place_id=places.id AND tokens.case_id=?", self.id, self.id, current_case.id ]
       ).each do |token|
         token.destroy
       end
      #deleted_ids = ActiveRecord::Base.connection.execute("DELETE 
      #  FROM  tokens
      #  USING places, arcs
      #  WHERE   arcs.from_is_place=true AND arcs.transition_id=#{self.id} AND 
      #    arcs.place_id=places.id AND arcs.type='ResetArc' AND 
      #    tokens.place_id=places.id AND tokens.case_id=#{current_case.id}
      #  RETURNING tokens.id
      # ").result
   end
  end
  
  class AutomaticTransition < Transition
    
    def trigger_fire
      Merb.logger.info("[#{log_name}] Trigger fire.")
      
      begin
        if xslt_url
          xslt_auto = XML::XSLT.new
          xslt_auto.xsl = xslt_path
        end
        #enable NOP transitions with only ONE incoming place (ugly)
        if !xslt_url && incoming_places.size == 1
          AutofireFileGenerator.initializeXSLT(self)
          xslt_auto = XML::XSLT.new
          xslt_auto.xsl = xslt_path
        end

        token_combinations.each do |comb|
          #after having fired a token combination, other combinations don't have to be valid anymore
          next if !Combination.exists?(comb.id)
          
          new_token_value = nil
          if xslt_url || (!xslt_url && incoming_places.size == 1 && comb.tokens.any?{|t|!t.value.blank?})
            AutofireFileGenerator.create_token_files(comb.tokens, base_path)
            AutofireFileGenerator.initializeXML(self)
            
            xslt_auto.xml = xml_path
            #workaround for ruby-xslt 0.9.6 bug
            #new_token_value = xslt_auto.serve
            new_token_value = `xsltproc #{xslt_path} #{xml_path}`
          else 
            new_token_value = comb.tokens.empty? ?  "" : comb.tokens[0].value
          end
          
          fire(comb.tokens, {:value => new_token_value})
        
          #reload
          
          current_case = comb.tokens.first.case unless comb.tokens.empty? #TODO why can tokens be empty?
          
          PNEngine::CsvLogger.log_transition("automatic_fire", Time.new.to_s, "", id, name, action, current_case.id, petrinet.id)
          #Merb.logger.info("[#{log_name}] Automatically fired.")
        end
      rescue Exception => e
          Merb.logger.error("[#{self.log_name}] #{e}")
          Merb.logger.error(e.backtrace.join("\n"))
      end
    end
  end
    
  class SendTransition < Transition
    def validate
      #TODO validate exactly 1 incoming arc
    end
    
    def trigger_fire
      Merb.logger.info("[#{log_name}] Trigger fire.")
      transition = self
      
      token_combinations.each do |comb|
        token = comb.tokens.first
        
        url = URI.parse(send_to_locator ? comb.tokens.first.values[send_to_locator] : send_to)
        #TODO raise error if req not successful to rollback
        res = Net::HTTP.start(url.host, url.port) do |http|
          req = Net::HTTP::Post.new(url.path)
          req["ACCEPT"] = "application/xml"
          req.basic_auth 'account', 'password'
          #set redirect url
          if transition.redirect_to_transition && transition.redirect_to_locator
            redirect_transition = petrinet.find_transition(transition.redirect_to_transition)
            token.set_locator(transition.redirect_to_locator, redirect_transition.absolute_url(:case => token.case ))
          end
          #build envelope around token value
          req.body = "<root>" + token.value + "</root>"
          http.request(req)
        end
        if transition.location_locator
          token.set_locator(transition.location_locator, res["LOCATION"])
        end
        fire(comb.tokens, {:value => token.value})
      end
    end
  end

  class ReceiveTransition < Transition
    def fire(pull_tokens, attributes, options = {})
      if xslt_url
        #AutofireFileGenerator.initializeXSLT(self)
        xslt_auto = XML::XSLT.new
        xslt_auto.xsl = xslt_path

        AutofireFileGenerator.create_token_files(pull_tokens, base_path)
        AutofireFileGenerator.initializeXML(self)
        xslt_auto.xml = xml_path

        new_token_value = nil
        Merb.logger.info("Before transformation of #{base_path} #{xml_path}")
        #workaround for ruby-xslt 0.9.6 bug
        #new_token_value = xslt_auto.serve
        new_token_value = `xsltproc #{xslt_path} #{xml_path}`
        Merb.logger.info("After transformation of #{base_path} #{xml_path}")
        attributes[:value] = new_token_value
      end
      super(pull_tokens, attributes, options)
    end
  end #class ReceiveTransition
  
  class RefererTransition < Transition
    
    def validate_arc(arc)
      #allow incoming arcs if type=read
      (arc.from_is_place && type=='read') ? true : false      
    end
  end
  
end #module PNEngine
